package com.atguigu.gulimall.seckill.service.impl;

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.atguigu.common.constant.OrderConstant;
import com.atguigu.common.constant.SeckillConstant;
import com.atguigu.common.to.SecKillSkuRedisTo;
import com.atguigu.common.to.mq.SeckillOrderTo;
import com.atguigu.common.utils.R;
import com.atguigu.common.vo.MemberRespVo;
import com.atguigu.common.vo.SkuInfoVo;
import com.atguigu.gulimall.seckill.feign.CouponFeignService;
import com.atguigu.gulimall.seckill.feign.ProductFeignService;
import com.atguigu.gulimall.seckill.interceptor.LoginUserInterceptor;
import com.atguigu.gulimall.seckill.service.SeckillService;
import com.atguigu.gulimall.seckill.vo.SeckillSessionsWithSkus;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.redisson.api.RSemaphore;
import org.redisson.api.RedissonClient;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * @author liuq
 * @version 1.0
 * @description:
 * @date 2021/3/9 22:38
 */
@Slf4j
@Service
public class SeckillServiceImpl implements SeckillService {

    @Autowired
    private CouponFeignService couponFeignService;

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private ProductFeignService productFeignService;

    @Autowired
    private RedissonClient redissonClient;

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Override
    public void uploadSeckillSkuLatest3Days() {
        // 扫描需要上架的秒杀商品
        R session = couponFeignService.getLates3DaySession();
        if (session.getCode() == 0) {
            List<SeckillSessionsWithSkus> data = (List<SeckillSessionsWithSkus>) session.getData(new TypeReference<List<SeckillSessionsWithSkus>>() {
            });
            if (!CollectionUtils.isEmpty(data)) {
                /*
                 缓存到redis
                 1、缓存活动信息
                */
                saveSessionInfos(data);
                // 2、缓存活动的关联商品信息
                saveSessionSkuInfos(data);
            }
        }
    }

    /**
     * 获取当前可以参与的秒杀商品的信息
     *
     * @return
     */
    @SentinelResource(value = "getCurrentSeckillSkus", blockHandler = "seckillSkusBlockHandler")
    @Override
    public List<SecKillSkuRedisTo> getCurrentSeckillSkus() {
        // 1、获取当前时间的所有秒杀场次
//        try (Entry entry = SphU.entry("seckillSkus")){
            long time = new Date().getTime();
            Set<String> keys = redisTemplate.keys(SeckillConstant.SESSIONS_CACHE_PREFIX + "*");
            for (String key : keys) {
                String replace = key.replace(SeckillConstant.SESSIONS_CACHE_PREFIX, "");
                String[] split = replace.split("_");
                Long start = Long.parseLong(split[0]);
                Long end = Long.parseLong(split[1]);
                if (time >= start && time <= end) {
                    // 获取当前场次需要的所有商品信息
                    List<String> range = redisTemplate.opsForList().range(key, -100, 100);
                    if (!CollectionUtils.isEmpty(range)) {
                        BoundHashOperations<String, String, String> operations = redisTemplate.boundHashOps(SeckillConstant.SKUKILL_SESSIONS_CACHE_PREFIX);
                        List<String> multiGet = operations.multiGet(range);
                        if (!CollectionUtils.isEmpty(multiGet)) {
                            List<SecKillSkuRedisTo> collect = multiGet.stream().map(item -> JSON.parseObject(item, SecKillSkuRedisTo.class)).collect(Collectors.toList());
                            return collect;
                        }
                    }
                    break;
                }
            }
//        } catch (BlockException e) {
//            log.info("alibaba Sentinel 自定义受保护资源进行熔断 Exception :", e.getMessage());
//        } finally {
//        }
        return null;
    }


    public List<SecKillSkuRedisTo> seckillSkusBlockHandler(BlockException b){
        log.info("使用注解之后调用的熔断方法， 异常信息 {}", b);
        return null;
    }

    @Override
    public SecKillSkuRedisTo getSkuSeckillInfo(Long skuId) {
        // 1、找到所有的需要参与秒杀的商品的key
        BoundHashOperations<String, String, String> operations = redisTemplate.boundHashOps(SeckillConstant.SKUKILL_SESSIONS_CACHE_PREFIX);
        Set<String> keys = operations.keys();
        String regx = "\\d" + "_" + skuId;
        if (!CollectionUtils.isEmpty(keys)) {
            for (String key : keys) {
                // 和当前的传入的ID进行匹配，找到第一个就return， 因为是匹配到的第一个
                if (Pattern.matches(regx, key)) {
                    String json = operations.get(key);
                    SecKillSkuRedisTo redisTo = JSON.parseObject(json, SecKillSkuRedisTo.class);
                    long time = new Date().getTime();
                    long startTime = redisTo.getStartTime();
                    long endTime = redisTo.getEndTime();
                    // 不在秒杀时间内时不返回随机秒杀码
                    if (time < startTime || time > endTime) {
                        redisTo.setRandomCode(null);
                    }
                    return redisTo;
                }
            }
        }
        return null;
    }

    @Override
    public String kill(String killId, String randomToken, Integer num) {

        MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();

        // 获取当前秒杀商品的详细信息
        BoundHashOperations<String, String, String> operations = redisTemplate.boundHashOps(SeckillConstant.SKUKILL_SESSIONS_CACHE_PREFIX);
        String killSku = operations.get(killId);
        if (StringUtils.isBlank(killSku)) {
            // 没有获取到秒杀商品的信息
            return null;
        } else {
            SecKillSkuRedisTo skuRedisTo = JSON.parseObject(killSku, SecKillSkuRedisTo.class);
            // 1、校验合法性
            // 秒杀时间
            long now = new Date().getTime();
            long startTime = skuRedisTo.getStartTime();
            long endTime = skuRedisTo.getEndTime();
            if (now >= startTime && now <= endTime) {
                // 2、校验秒杀商品的随机码
                String randomCode = skuRedisTo.getRandomCode();
                String redisSkuKey = skuRedisTo.getPromotionSessionId() + "_" + skuRedisTo.getSkuId();
                if (randomCode.equals(randomToken) && redisSkuKey.equals(killId)) {
                    //3、验证购物数量是否超出限制
                    if (num <= skuRedisTo.getSeckillLimit().intValue()) {
                        // 4、验证这个人是不是已经买过了。秒杀成功就在redis中站位  userId_SessionId_skuId
                        String hasKillKey = memberRespVo.getId() + "_" + redisSkuKey;
                        // 设置自动过期，活动完成时间 - 当前时间
                        Boolean absent = redisTemplate.opsForValue().setIfAbsent(hasKillKey, num.toString(), endTime - now, TimeUnit.MILLISECONDS);
                        if (absent) {
                            // 说明没买过
                            RSemaphore semaphore = redissonClient.getSemaphore(SeckillConstant.SKU_STOCK_SEMAPHORE_PREFIX + randomToken);
                            try {
                                // 只尝试100毫秒进行尝试获取信号量
                                // 或者直接走代码  能获取到就获取，获取不到就直接返回
                                boolean acquire = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS);
                                if (acquire) {
                                    // 秒杀成功 发送消息给MQ说明下单成功
                                    String timeId = IdWorker.getTimeId();
                                    SeckillOrderTo seckillOrderTo = new SeckillOrderTo();
                                    seckillOrderTo.setOrderSn(timeId);
                                    seckillOrderTo.setPromotionSessionId(skuRedisTo.getPromotionSessionId());
                                    seckillOrderTo.setNum(num);
                                    seckillOrderTo.setSeckillPrice(skuRedisTo.getSeckillPrice());
                                    seckillOrderTo.setSkuId(skuRedisTo.getSkuId());
                                    seckillOrderTo.setMumberId(memberRespVo.getId());

                                    // com.sherlock.gmall.order.config.OrderMQConfig.orderSeckillOrderQueue

                                    rabbitTemplate.convertAndSend(OrderConstant.ORDER_EVENT_EXCHANGE, SeckillConstant.ORDER_SECKILL_ROUTING_KEY_NAME,seckillOrderTo, new CorrelationData(timeId));
                                    return timeId;
                                }
                            } catch (InterruptedException e) {
                                log.info("获取信号量失败, {}", e.getMessage());
                                return null;
                            }

                        } else {
                            // 已经买过了
                            return null;
                        }
                    }
                } else {
                    return null;
                }
            } else {
                // 过了秒杀期
                return null;
            }

        }
        return null;
    }

    private void saveSessionInfos(List<SeckillSessionsWithSkus> sessions) {
        if (CollectionUtils.isEmpty(sessions)) {
            return;
        }
        sessions.forEach(session -> {
            Long startTime = session.getStartTime().getTime();
            Long endTime = session.getEndTime().getTime();
            String key = SeckillConstant.SESSIONS_CACHE_PREFIX + startTime + "_" + endTime;
            Boolean hasKey = redisTemplate.hasKey(key);
            // 保持幂等性，不重复往redis里面存储
            if (!hasKey) {
                // 场次ID + SKU id
                List<String> collect = session.getRelationSkus().stream().map(item -> item.getPromotionSessionId() + "_" + item.getSkuId()).collect(Collectors.toList());
                redisTemplate.opsForList().leftPushAll(key, collect);
            }
        });
    }

    private void saveSessionSkuInfos(List<SeckillSessionsWithSkus> sessions) {
        if (CollectionUtils.isEmpty(sessions)) {
            return;
        }
        // 准备redis的hash操作
        BoundHashOperations<String, String, String> operations = redisTemplate.boundHashOps(SeckillConstant.SKUKILL_SESSIONS_CACHE_PREFIX);
        sessions.forEach(session -> {
            session.getRelationSkus().forEach(seckillSkuVo -> {
                Boolean hasSku = operations.hasKey(seckillSkuVo.getPromotionSessionId() + "_" + seckillSkuVo.getSkuId());
                if (!hasSku) {

                    // 缓存商品
                    SecKillSkuRedisTo redisTo = new SecKillSkuRedisTo();

                    //1、 sku的基本信息
                    R<SkuInfoVo> skuInfo = productFeignService.getSkuInfo(seckillSkuVo.getSkuId());
                    if (skuInfo.getCode() == 0) {
                        SkuInfoVo skuInfoVo = skuInfo.getData(new TypeReference<SkuInfoVo>() {
                        });
                        redisTo.setSkuInfo(skuInfoVo);
                    }

                    //2、 sku的秒杀信息
                    BeanUtils.copyProperties(seckillSkuVo, redisTo);

                    //3、 设置商品的秒杀时间信息
                    redisTo.setStartTime(session.getStartTime().getTime());
                    redisTo.setEndTime(session.getEndTime().getTime());

                    //4、 设置商品的随机码
                    String randomToken = UUID.randomUUID().toString().replace("-", "");
                    redisTo.setRandomCode(randomToken);

                    //5、 放入缓存(上架商品)
                    String S = JSON.toJSONString(redisTo);
                    operations.put(seckillSkuVo.getPromotionSessionId() + "_" + seckillSkuVo.getSkuId(), S);

                    //6、 引入分布式信号量  使用库存数量作为分布式信号量的数量 作用就是限流(限制数量) key引入随机码是为了在页面展示的时候带上这个，防止别人恶意抢商品，但是如果没有这个随机key就不能抢
                    // 如果当前场次的商品的库存信息已经上架那么就不需要再次上架了
                    RSemaphore semaphore = redissonClient.getSemaphore(SeckillConstant.SKU_STOCK_SEMAPHORE_PREFIX + randomToken);
                    semaphore.trySetPermits(seckillSkuVo.getSeckillCount().intValue());
                }
            });
        });
    }
}
